import json, os, sys, socket, threading, pickle, shelve
from utils import DUtils, Logger
from traceback import print_exc

logger_manager = Logger(__name__, "MainThread")

class Session(threading.Thread):
    def __init__(self, uid, sock: socket.socket, addr) -> None:
        super().__init__(name="cli_" + uid, daemon=True)
        self.uid = uid
        self.sock = sock
        self.uaddr = addr
        self.logger = logger_manager.new(self.name)
        # self.config = pickle.load(open(os.path.join('users', self.uid), 'rb'))

    def run(self) -> None:
        try:
            cs.db["uid_pool"][self.uid]["state"] = "online"
            self.sock.sendall(DUtils.login_success(pickle.dumps(cs.db["uid_pool"][self.uid])))
            self.logger.info("user login successfully:", self.uaddr, self.uid)
            # if len(cs.msg_db[self.uid]["history"]):
            #     while len(cs.msg_db[self.uid]["history"]):
            #         func, f_ug_id, t_ug_id, text = cs.msg_db[self.uid]["history"].pop(0)
            #         self.send_message(func, f_ug_id, t_ug_id, text)
            if len(cs.msg_db[self.uid]["unread"]):
                while len(cs.msg_db[self.uid]["unread"]):
                    func, f_ug_id, t_ug_id, text = cs.msg_db[self.uid]["unread"].pop(0)
                    self.send_message(func, f_ug_id, t_ug_id, text)
            while True:
                self.parse(DUtils.parse(self.recv()))
        except Exception as ept:
            print_exc()
            self.parse((0xff, self.uid))
    
    def send_message(self, func:int, f_ug_id:int, t_ug_id:int, text:str):
        if func == 0x00:
            t_uid = str(t_ug_id)
            if t_uid not in cs.sessions.keys():
                cs.msg_db[t_uid]["unread"].append((func, f_ug_id, t_ug_id, text))
            else:
                # cs.msg_db[t_uid]["history"].append((func, f_ug_id, t_ug_id, text))
                cs.sessions[t_uid].sock.sendall(DUtils.message_session(text, 0, uid=f_ug_id))

    def parse(self, p):
        if p is None:return
        if p[0] == 0x02:
            self.send_message(p[1][0], int(self.uid), p[1][1], p[1][2])
        elif p[0] == 0xff:
            # pickle.dump(self.config, open(os.path.join('users', self.uid), 'wb'))
            self.logger.info("user logout:", self.uaddr, self.uid)
            cs.db["uid_pool"][self.uid]["state"] = "offline"
            cs.sessions.pop(self.uid)
            del self
            sys.exit()
    
    def recv(self):
        size = int.from_bytes(self.sock.recv(2), 'big')
        return self.sock.recv(size)

class Terminal(threading.Thread):
    def __init__(self, name:str) -> None:
        super().__init__(name="ter_"+name, daemon=True)
        self.logger = logger_manager.new(self.name)
    
    def run(self) -> None:
        while True:
            try:
                text = sys.stdin.readline().split(' ')
                if text[0].strip() in ["exit", "quit", "q"]:
                    cs.stop()
                elif text[0].strip() == "show":
                    # if text[1].strip() == "thread_pool":
                    #     self.logger.print(cs.thread_pool)
                    # elif text[1].strip() == "pool":
                    #     self.logger.print(cs.pool)
                    # else:
                        self.logger.print(eval(text[1].strip()))
                elif text[0].strip() == "exec":
                    exec(text[1].strip())
                elif text[0].strip() == "data":
                    if text[1].strip() == "remove":
                        if text[2].strip() in cs.db["uid_pool"]["uid_list"]:
                            cs.db["uid_pool"]["uid_list"].remove(text[2].strip())
                            cs.db["uid_pool"].pop(text[2].strip())
                        else:
                            self.logger.warning("cannot find user", text[2].strip(), ", remove failed!")
                    elif text[1].strip() == "add":
                        if text[2].strip() in cs.db["uid_pool"]["uid_list"]:
                            self.logger.warning("user has already been in pool", text[2].strip(), ", add failed!")
                        else:
                            cs.db["uid_pool"][text[2].strip()] = {
                                "name": text[3].strip(),
                                "pwd": text[4].strip(),
                                "state": text[5].strip(),
                                "host": []
                            }
                            cs.db["uid_pool"]["uid_list"].append(text[2].strip())
                            pickle.dump({
                                "history": [],
                                "unread": []
                            }, open(os.path.join('users', text[2].strip()), 'xb'))
            except:
                print_exc()
 
class ChatServer:
    def __init__(self) -> None:
        self.db = {}
        self.msg_db = {}
        self.sessions:dict[str, Session] = {}
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.terminal = Terminal("ChatServerMain")
        self.terminal.start()
    
    def recv(self, sock: socket.socket):
        size = int.from_bytes(sock.recv(2), 'big')
        return sock.recv(size)
    
    # def private_send(self, from_uid:str, to_uid:str, text:str):
    
    def load(self):
        self.db = json.load(open("config.json", 'r', encoding='utf-8'))
        for i in os.listdir("users"):
            if os.path.isfile(os.path.join("users", i)):
                self.msg_db[i] = pickle.load(open(os.path.join("users", i), 'rb'))
        if type(self.db) != dict or type(self.msg_db) != dict:
            logger_manager.fatal("cannot load configure, please check if the 'config.json' was complete")
            sys.exit(1)
        self.sock.bind(("0.0.0.0", 44000))
        self.sock.listen(10)
        while True:
            sock, addr = self.sock.accept()
            pack = self.recv(sock)
            flag, info = DUtils.parse(pack)
            if flag == 0x0:
                self.on_user_login(info, addr, sock, pack)
            elif flag == 0x1:
                self.on_user_register(info, addr, sock, pack)

    def on_user_login(self, info, addr: tuple, sock: socket.socket, pack: bytes):
        if info is None:
            logger_manager.warning("user login failed:", addr)
            sock.send(DUtils.login_failed(0, "pack error"+pack.decode('utf-8')))
            return
        uid_i, pwd = info
        uid = str(uid_i)
        if uid not in self.db["uid_pool"]["uid_list"]:
            logger_manager.warning("user hasn't signed up, login failed:", addr)
            sock.send(DUtils.login_failed(uid_i, "user " + uid + " hasn't signed up yet"))
            return
        userinfo = self.db["uid_pool"][uid]
        if userinfo["pwd"] != pwd:
            logger_manager.warning("user pwd error!", addr)
            sock.send(DUtils.login_failed(uid_i, "pwd error"))
            return
        self.db["uid_pool"][uid]["host"] = addr
        self.sessions[uid] = Session(uid, sock, addr)
        self.sessions[uid].start()
    
    def on_user_register(self, info, addr: tuple, sock: socket.socket, pack: bytes):
        if info is None:
            logger_manager.warning("user register failed:", addr)
            sock.send(DUtils.register_failed(0, "pack error"+pack.decode('utf-8')))
            return
        uid_i, pwd = info
        uid = str(uid_i)
        if uid in self.db["uid_pool"]["uid_list"]:
            logger_manager.warning("user has already signed up, register failed:", addr)
            sock.send(DUtils.register_failed(uid_i, "user " + uid + " has already signed up"))
            return
        self.db["uid_pool"]["uid_list"].append(uid)
        self.db["uid_pool"][uid] = ChatServer.make_profile(uid, pwd, addr)
        pickle.dump({
            "history": [],
            "unread": []
        }, open(os.path.join('users', uid), 'xb'))
        logger_manager.info("user register successfully:", addr, uid)
        sock.sendall(DUtils.register_success(pickle.dumps(self.db["uid_pool"][uid])))
        self.save()

    def dump(self):
        r = {}
        for k, v in self.sessions.items():
            r[k] = {
                "uid": v.uid,
                "addr": v.uaddr,
            }
        json.dump(r, open("dump.json", 'w', encoding='utf-8'))
    
    def load_backup(self):pass
        # self.thread_pool = pickle.load(open("backup.dump", 'rb'))
    
    def save(self):
        json.dump(self.db, open("config.json", 'w', encoding='utf-8'))
        for i in self.msg_db.keys():
            pickle.dump(self.msg_db[i], open(os.path.join("users", i), 'wb'))
    
    def stop(self, code=0):
        self.sock.close()
        if not code:self.dump()
        self.save()
        sys.exit(code)
    
    @staticmethod
    def make_profile(uid:str, pwd:str, addr:tuple):
        return {
            "name": "user_"+uid,
            "pwd": pwd,
            "state": "offline",
            "host": addr
        }

if __name__ == "__main__":
    cs = ChatServer()
    try:
        cs.load()
    except KeyboardInterrupt:
        cs.stop()
    except Exception as e:
        logger_manager.fatal("An error has occurred while the server running:")
        print_exc()
        cs.stop(1)